#include "VideoPlayer.h"

#include "CVTextureCache.h"
#include "CMVideoSampling.h"
#include "GlesHelper.h"

#import <AVFoundation/AVFoundation.h>


static void* _ObserveItemStatusContext = (void*)0x1;
static void* _ObservePlayerItemContext = (void*)0x2;


@implementation VideoPlayerView
+ (Class)layerClass
{
	return [AVPlayerLayer class];
}

- (AVPlayer*)player
{
	return [(AVPlayerLayer*)[self layer] player];
}


- (void)setPlayer:(AVPlayer*)player
{
	[(AVPlayerLayer*)[self layer] setPlayer:player];
}

- (void)dealloc
{
	self.player = nil;
}
@end


@implementation VideoPlayer
{
	AVPlayerItem*   _playerItem;
	AVPlayer*       _player;

	AVAssetReader*              _reader;
	AVAssetReaderTrackOutput*   _videoOut;

	CMSampleBufferRef           _cmSampleBuffer;
	CMVideoSampling             _videoSampling;

	CMTime          _duration;
	CMTime          _curTime;
	CMTime          _curFrameTimestamp;
	CMTime          _lastFrameTimestamp;
	CGSize          _videoSize;

	BOOL            _playerReady;

	// we need to have both because the order of asset/item getting ready is not strict
	BOOL            _assetReady;
	BOOL            _itemReady;
}

@synthesize delegate;

- (BOOL)readyToPlay			{ return _playerReady; }
- (CGSize)videoSize			{ return _videoSize; }
- (CMTime)duration			{ return _duration; }
- (float)durationSeconds	{ return CMTIME_IS_VALID(_duration) ? (float)CMTimeGetSeconds(_duration) : 0.0f; }


+ (BOOL)CanPlayToTexture:(NSURL*)url	{ return [url isFileURL]; }


- (void)reportError:(NSError*)error category:(const char*)category
{
	::printf("[%s]Error: %s\n", category, [[error localizedDescription] UTF8String]);
	::printf("%s\n", [[error localizedFailureReason] UTF8String]);
}
- (void)reportErrorWithString:(const char*)error category:(const char*)category
{
	::printf("[%s]Error: %s\n", category, error);
}

- (id)init
{
	if( (self = [super init]) )
	{
		_duration = _curTime = kCMTimeZero;
		_curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;
	}
	return self;
}

- (void)cleanupCVTextureCache
{
	if(_cmSampleBuffer)
	{
		CFRelease(_cmSampleBuffer);
		_cmSampleBuffer = 0;
	}
	CMVideoSampling_Uninitialize(&_videoSampling);
}

- (void)cleanupAssetReader
{
	if(_reader)
		[_reader cancelReading];

	_reader = nil;
	_videoOut = nil;
}

- (void)cleanupPlayer
{
	if(_player)
	{
		[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
		[_player.currentItem removeObserver:self forKeyPath:@"status"];
		[_player removeObserver:self forKeyPath:@"currentItem"];
		[_player pause];
		_player = nil;
	}

	if(_playerItem)
	{
		[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
		_playerItem = nil;
	}
}


- (void)unloadPlayer
{
	[self cleanupCVTextureCache];
	[self cleanupAssetReader];
	[self cleanupPlayer];

	_videoSize = CGSizeMake(0,0);
	_duration = _curTime = kCMTimeZero;
	_curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;

	self->_playerReady = self->_assetReady = self->_itemReady = NO;
}


- (BOOL)loadVideo:(NSURL*)url
{
	AVURLAsset* asset = [AVURLAsset URLAssetWithURL:url options:nil];
	if(!asset) return NO;

	NSArray* requestedKeys = @[@"tracks", @"playable"];
	[asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:
		^{
			dispatch_async(dispatch_get_main_queue(), ^{ [self prepareAsset:asset withKeys:requestedKeys]; });
		}
	];
	return YES;
}

- (BOOL)_play:(VideoPlayerView*)view
{
	if(!_playerReady)
		return NO;

	if(view)    view.player = _player;
	else        [self prepareReader];

	// do not do seekTo and setRate here, it seems that http streaming may hang sometimes if you do so. go figure
	_curFrameTimestamp = _lastFrameTimestamp = kCMTimeZero;
	[_player play];

	return YES;
}

- (BOOL)playToView:(VideoPlayerView*)view	{ return [self _play:view]; }
- (BOOL)playToTexture						{ return [self _play:nil]; }

- (BOOL)isPlaying	{ return _playerReady && _player.rate != 0.0f; }

- (void)pause	{ if(_playerReady && _player.rate != 0.0f) [_player pause]; }
- (void)resume	{ if(_playerReady && _player.rate == 0.0f) [_player play]; }

- (void)rewind						{ [self seekToTimestamp:kCMTimeZero]; }
- (void)seekTo:(float)timeSeconds	{ [self seekToTimestamp:CMTimeMakeWithSeconds(timeSeconds, 1)]; }
- (void)seekToTimestamp:(CMTime)time
{
	[_player seekToTime:time];
	_curFrameTimestamp = _lastFrameTimestamp = time;
}

- (intptr_t)curFrameTexture
{
	if(!_reader)
		return 0;

	intptr_t curTex = CMVideoSampling_LastSampledTexture(&_videoSampling);

	CMTime time = [_player currentTime];

	// if we have changed audio route and due to current category apple decided to pause playback - resume automatically
	if(_AudioRouteWasChanged && _player.rate == 0.0f)
		_player.rate = 1.0f;

	if(CMTimeCompare(time, _curTime) == 0 || _reader.status != AVAssetReaderStatusReading)
		return curTex;

	_curTime = time;
	while(_reader.status == AVAssetReaderStatusReading && CMTimeCompare(_curFrameTimestamp, _curTime) <= 0)
	{
		if(_cmSampleBuffer)
			CFRelease(_cmSampleBuffer);

		// TODO: properly handle ending
		_cmSampleBuffer = [_videoOut copyNextSampleBuffer];
		if(_cmSampleBuffer == 0)
		{
			[self cleanupCVTextureCache];
			return 0;
		}

		_curFrameTimestamp = CMSampleBufferGetPresentationTimeStamp(_cmSampleBuffer);
	}

	if(CMTimeCompare(_lastFrameTimestamp, _curFrameTimestamp) < 0)
	{
		_lastFrameTimestamp = _curFrameTimestamp;

		size_t w, h;
		curTex = CMVideoSampling_SampleBuffer(&_videoSampling, _cmSampleBuffer, &w, &h);
		_videoSize = CGSizeMake(w, h);
	}

	return curTex;
}

- (BOOL)setAudioVolume:(float)volume
{
	if(!_playerReady)
		return NO;

	NSArray* audio = [_playerItem.asset tracksWithMediaType:AVMediaTypeAudio];
	NSMutableArray* params = [NSMutableArray array];
	for(AVAssetTrack* track in audio)
	{
		AVMutableAudioMixInputParameters* inputParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
		[inputParams setVolume:volume atTime:kCMTimeZero];
		[inputParams setTrackID:[track trackID]];
		[params addObject:inputParams];
	}

	AVMutableAudioMix* audioMix = [AVMutableAudioMix audioMix];
	[audioMix setInputParameters:params];

	[_playerItem setAudioMix:audioMix];

	return YES;
}

- (void)playerItemDidReachEnd:(NSNotification*)notification
{
	[delegate onPlayerDidFinishPlayingVideo];
}

static bool _AudioRouteWasChanged = false;
- (void)audioRouteChanged:(NSNotification*)notification
{
	_AudioRouteWasChanged = true;
}


- (void)observeValueForKeyPath:(NSString*)path ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
	BOOL reportPlayerReady = NO;

	if(context == _ObserveItemStatusContext)
	{
		AVPlayerStatus status = (AVPlayerStatus)[[change objectForKey:NSKeyValueChangeNewKey] integerValue];
		switch(status)
		{
			case AVPlayerStatusUnknown:
			break;

			case AVPlayerStatusReadyToPlay:
			{
				NSArray* video = [_playerItem.asset tracksWithMediaType:AVMediaTypeVideo];
				if([video count])
					_videoSize = [(AVAssetTrack*)[video objectAtIndex:0] naturalSize];

				_duration			= [_playerItem duration];
				_assetReady			= YES;
				reportPlayerReady	= _itemReady;
			}
			break;

			case AVPlayerStatusFailed:
			{
				AVPlayerItem *playerItem = (AVPlayerItem*)object;
				[self reportError:playerItem.error category:"prepareAsset"];
			}
			break;
		}
	}
	else if(context == _ObservePlayerItemContext)
	{
		if( [change objectForKey:NSKeyValueChangeNewKey] != (id)[NSNull null] )
		{
			_itemReady = YES;
			reportPlayerReady = _assetReady;
		}
	}
	else
	{
		[super observeValueForKeyPath:path ofObject:object change:change context:context];
	}

	if(reportPlayerReady)
	{
		_playerReady = YES;
		[delegate onPlayerReady];
	}
}

- (void)prepareAsset:(AVAsset*)asset withKeys:(NSArray*)requestedKeys
{
	// check succesful loading
	for(NSString* key in requestedKeys)
	{
		NSError* error = nil;
		AVKeyValueStatus keyStatus = [asset statusOfValueForKey:key error:&error];
		if(keyStatus == AVKeyValueStatusFailed)
		{
			[self reportError:error category:"prepareAsset"];
			return;
		}
	}

	if(!asset.playable)
	{
		[self reportErrorWithString:"Item cannot be played" category:"prepareAsset"];
		return;
	}

	if(_playerItem)
	{
		[_playerItem removeObserver:self forKeyPath:@"status"];
		[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];

		_playerItem = nil;
	}

	_playerItem = [AVPlayerItem playerItemWithAsset:asset];
	[_playerItem	addObserver:self forKeyPath:@"status"
					options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
					context:_ObserveItemStatusContext
	];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:)
										  name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem
	];

	if(!_player)
	{
		_player = [AVPlayer playerWithPlayerItem:_playerItem];
		[_player	addObserver:self forKeyPath:@"currentItem"
					options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
					context:_ObservePlayerItemContext
		];

		[_player setAllowsExternalPlayback:NO];

		// we want to subscribe to route change notifications, for that we need audio session active
		// and in case FMOD wasnt used up to this point it is still not active
		[[AVAudioSession sharedInstance] setActive:YES error:nil];
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChanged:)
											  name:AVAudioSessionRouteChangeNotification object:nil
		];
	}

	if(_player.currentItem == _playerItem)
		[_player replaceCurrentItemWithPlayerItem:_playerItem];
	else
		[_player seekToTime:kCMTimeZero];
}

- (BOOL)prepareReader
{
	if(!_playerReady)
		return NO;

	[self cleanupAssetReader];

	AVURLAsset* asset = (AVURLAsset*)_playerItem.asset;
	if(![asset.URL isFileURL])
	{
		[self reportErrorWithString:"non-file url. no video to texture." category:"prepareReader"];
		return NO;
	}

	NSError* error = nil;
	_reader = [AVAssetReader assetReaderWithAsset:_playerItem.asset error:&error];
	if(error)
		[self reportError:error category:"prepareReader"];

	_reader.timeRange = CMTimeRangeMake(kCMTimeZero, _duration);


	AVAssetTrack* videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];

	NSDictionary* options = @{ (NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA) };
	_videoOut = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];
	_videoOut.alwaysCopiesSampleData = NO;

	if(![_reader canAddOutput:_videoOut])
	{
		[self reportErrorWithString:"canAddOutput returned false" category:"prepareReader"];
		return NO;
	}
	[_reader addOutput:_videoOut];

	if(![_reader startReading])
	{
		[self reportError:[_reader error] category:"prepareReader"];
		return NO;
	}

	[self cleanupCVTextureCache];
	CMVideoSampling_Initialize(&_videoSampling);

	return NO;
}

@end
